# Copyright (c) 2007, Enrico Franchi
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the University of California, Berkeley nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import subprocess
import itertools
import tempfile
import MacOS

import options, targets
import Authorization
from Authorization import Authorization, kAuthorizationFlagDestroyRights

import sys, os
from os.path import join, dirname
location = join(dirname(sys.argv[0]), '..')

sys.path.insert(0, location)

try:
    import cStringIO as StringIO
except ImportError:
    import StringIO

class FailedCommandException(Exception):
    pass

AUTHORIZEDTOOL = r'''#!/usr/bin/env ruby

require 'pty'

args = $*
if args.size == 0
  puts "You should provide a command to run"
  exit -1
end

begin
  Process::UID.change_privilege(0)
  
  command = args.join(' ')
  command.untaint
  PTY.spawn(command) do |read, write, pid|
    begin 
      # write.sync = true
      read.each_line do |line|
        puts line
      end
      npid, status = Process.wait2(pid, Process::WNOHANG)
      if status != 0
        msg = "Error: command <#{command}>(#{npid}) went out with exit code #{status}!"
        puts msg
        STDERR.puts msg
      end
    rescue PTY::ChildExited
      raise
    end
  end
rescue Errno::EPERM
  STDERR.puts "Error: Can't set uid 0"
  exit(-1)
end
'''


class PortCommand(object):
    '''You can call a port command with all the usual targets. 
    
        pc = PortCommand(somepath)
        pc.list()
        pc.install(port)
        ...
        
        In fact we are doing some dynamic tricks with the _run function.
    '''
    def __init__(self, path, 
                target=targets.LIST, 
                options=None, catcher=None):
        """path is the path of the port cli application"""
        self._path    = path
        self.target   = target
        self._options = options or []
        if catcher is None:
            self.catcher  = StringIO.StringIO()
        else:
            self.catcher  = catcher
        
    def addOption(self, option, arg=None):
        """addOption adds an option to the list of options passed to the command.
        
        The options that can be passed are only 'general' options that are meant
        to be put *before* the target. Read the port manual for more details.
        """
        self._options.append(option)
        if arg is not None:
            self._options.append(arg)
    
    def clearOptions(self):
        """Resets the options array"""
        self._options = []
        
    def target():
        doc = "The target property."
        def fget(self):
            return self._target
        def fset(self, value):
            if value in targets.targets.values():
                self._target = value
            else:
                err_message =   'Type must be one of ' + \
                                ", ".join(targets.targets.values()) + \
                                ", however, it was %s." % value                              
                raise TypeError(err_message)
        # no del: we don't want to del it
        return locals()
    target = property(**target())
    
    def appendToCatcher(self, line):
        try:
            bound = self.catcher.add
        except AttributeError:
            self.catcher.write(line)
        else:
            bound(line)
            
    def buildArgs(self, port_args):
        args = [self._path]
        args.extend(self._options)
        args.append(self.target)
        args.extend(port_args)
        return args
    
    def _run(self, *port_args):
        args = self.buildArgs(port_args)
        print 'Called `%s`' % ' '.join(args)
        self.process = subprocess.Popen(args,
                stdout  = subprocess.PIPE,
                stderr  = subprocess.STDOUT)
        for line in self.process.stdout:
            self.appendToCatcher(line)
        
    def __getattr__(self, value):
        self.target = value # check performed in the setter method
        return self._run
        
    def getAllData(self):
        try:
            self.catcher.seek
            self.catcher.read
        except AttributeError:
            return ''
        else:
            self.catcher.seek(0)
            return self.catcher.read()
        
class AuthorizedPortCommand(PortCommand):
    def _run(self, *port_args):
        args = self.buildArgs(port_args)
        print 'Called `%s`' % ' '.join(args)
        try:
            auth = Authorization(destroyflags=(kAuthorizationFlagDestroyRights,))
            fd, name = tempfile.mkstemp('.rb')
            os.write(fd, AUTHORIZEDTOOL)
            os.close(fd)
            os.chmod(name, 0700)
            try:
                pipe = auth.executeWithPrivileges(name, *args)
                for line in pipe:   
                    self.appendToCatcher(line)
                pipe.close()
            finally:
                os.unlink(name)
        except MacOS.Error, e:
            self.appendToCatcher(e[2])
        
if __name__ == '__main__':
    # pc = PortCommand('/opt/local/bin/port')
    # pc.list()
    pc = AuthorizedPortCommand('/opt/local/bin/port')
    # pc.install('cpio')
    pc.uninstall('cpio')
    print pc.getAllData()
        